package net.pulseproject.router.modules.logic.beatstabilizer;

import java.util.Timer;

import net.pulseproject.commons.misc.TimerTaskWrapperX;
import net.pulseproject.commons.misc.TimerTaskWrapperX.TimerListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BeatStabilizer implements TimerListener {
	
	private static final Log LOG = LogFactory.getLog(BeatStabilizer.class);
	
	private final BeatStabilizerListener listener;
	private final BeatHistory history;
	private final long globalMinMs;
	private final int globalMaxMs;
	
	private final double frameDeviationMaxPercent; // precalculated ready2use max multiplicator: e.g.: 1.025
	private final double frameDeviationMinPercent; // precalculated ready2use min multiplicator: e.g.: 0.075

	private Timer timer;
	private long recentDuration;
	
	/**
	 * @param globalMinMs e.g.: x ms ==  60bpm
	 * @param globalMaxMs e.g.: x ms == 200bpm
	 * @param frameDeviationPercent e.g.: 2.5
	 */
	public BeatStabilizer(BeatStabilizerListener listener, BeatHistory history, BeatStabilizerConfig stabilizerConfig) {
		this.listener = listener;
		this.history = history;
		this.globalMinMs = stabilizerConfig.getGlobalMinMs();
		this.globalMaxMs = stabilizerConfig.getGlobalMaxMs();
		
		double frameDeviationPercentBy100 = stabilizerConfig.getFrameDeviationPercent() / 100.0D;
		this.frameDeviationMaxPercent = 1.0D + frameDeviationPercentBy100;
		this.frameDeviationMinPercent = 1.0D - frameDeviationPercentBy100;
	}

	public interface BeatStabilizerListener {
		void onBeatSignal();
	}

	public void start(long initDuration) {
		LOG.info("start(initDuration=" + initDuration + ")");
		
		this.recentDuration = initDuration;
		this.broadcastAndStartDelay();
	}

	public void stop() {
		LOG.info("stop()");
		this.stopTimer();
	}
	
	private void broadcastAndStartDelay() {
		LOG.debug("broadcastAndStartDelay()");
		
		final long rawDuration;
		final long historyAverageDuration = this.history.calcAverageDuration();
		
		if(historyAverageDuration == BeatHistory.NOT_ENOUGH_DATA) {
			LOG.trace("History got not enough data, using recentDuration [" + this.recentDuration + "] * frameDeviationMaxPercent [" + this.frameDeviationMaxPercent + "]");
			assert(this.recentDuration > 0);
			rawDuration = Math.round(this.recentDuration * this.frameDeviationMaxPercent);
			
		} else {
			LOG.trace("Using history data ... historyAverageDuration="+historyAverageDuration+", this.recentDuration="+this.recentDuration);
			
			if(historyAverageDuration < this.recentDuration) { // we have to be faster
				rawDuration = Math.max(Math.round(this.recentDuration * this.frameDeviationMinPercent), historyAverageDuration);
				
			} else if(historyAverageDuration > this.recentDuration) { // we have to be slower
				rawDuration = Math.min(Math.round(this.recentDuration * this.frameDeviationMaxPercent), historyAverageDuration);
				
			} else { // could that really be the case?!
				rawDuration = historyAverageDuration;
			}
		}
		
		final long duration = Math.min(Math.max(rawDuration, this.globalMinMs), this.globalMaxMs);
		LOG.trace("  rawDuration="+rawDuration+", duration="+duration+", historyAverageDuration="+historyAverageDuration);
		BeatStabilizerDebuggerGUiApp.debugDisplayNextGreenFrameWindow("[raw: "+rawDuration+" | real: "+duration+"]");
		this.recentDuration = duration;
		this.listener.onBeatSignal();
		this.delay(duration);
	}
	
	private void delay(long milliseconds) {
		LOG.debug("delay(milliseconds=" + milliseconds + ")");
		
		assert(this.timer == null);
		this.timer = new Timer();
		this.timer.schedule(new TimerTaskWrapperX(this), milliseconds);
	}
	
	@Override
	public void onTimerComplete() {
		LOG.warn("onTimerComplete() ... dispatching beat stabilized MIDI message");
		
		this.stopTimer();
		this.broadcastAndStartDelay();
	}

	private void stopTimer() {
		if(this.timer != null) {
			LOG.debug("stopTimer() ... really stopping ;)");
			this.timer.cancel();
			this.timer = null;
		}
	}
}
